package com.ksana.aop.aspectj;

import com.ksana.aop.ClassFilter;
import com.ksana.aop.MethodMatcher;
import com.ksana.aop.supprot.AbstractExpressionPointcut;
import com.ksana.aop.supprot.ExpressionPointcut;
import com.ksana.bean.factory.BeanFactory;
import com.ksana.bean.factory.BeanFactoryAware;
import com.ksana.bean.factory.config.ConfigurableBeanFactory;
import com.ksana.bean.factory.config.ConfigurableListableBeanFactory;
import com.ksana.util.ClassUtil;
import com.ksana.util.StringUtil;
import org.aspectj.weaver.tools.PointcutExpression;
import org.aspectj.weaver.tools.PointcutParser;
import org.aspectj.weaver.tools.PointcutPrimitive;

import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;

public class AspectJExpressionPointcut extends AbstractExpressionPointcut implements ClassFilter, MethodMatcher, BeanFactoryAware {
    private BeanFactory beanFactory;
    private ClassLoader classLoader;
    private PointcutExpression pointcutExpression;

    private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = new HashSet<>();

    static {
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.ARGS);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.REFERENCE);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.THIS);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.TARGET);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.WITHIN);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ANNOTATION);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_WITHIN);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ARGS);
        SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_TARGET);
    }

    public AspectJExpressionPointcut(String expression) {
        setExpression(expression);
    }

    @Override
    public boolean matches(Class<?> clazz) {
        obtainPointcutExpression();
        return pointcutExpression.couldMatchJoinPointsInType(clazz);
    }

    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        obtainPointcutExpression();
        return pointcutExpression.matchesMethodExecution(method).alwaysMatches();
    }

    private PointcutExpression obtainPointcutExpression() {
        if (pointcutExpression == null) {
            this.classLoader = determinePointcutClassLoader();
            this.pointcutExpression = buildPointcutExpression(this.classLoader);
        }

        return this.pointcutExpression;
    }

    private ClassLoader determinePointcutClassLoader() {
        if (this.beanFactory instanceof ConfigurableBeanFactory) {
            return this.beanFactory.getClass().getClassLoader();
        }

        return ClassUtil.getClassLoader();
    }

    private PointcutExpression buildPointcutExpression(ClassLoader classLoader) {
        PointcutParser parser = initializePointcutParser(classLoader);

        return parser.parsePointcutExpression(replaceBooleanOperators(resolveExpression()));
    }

    private PointcutParser initializePointcutParser(ClassLoader classLoader) {
        PointcutParser parser = PointcutParser.getPointcutParserSupportingSpecifiedPrimitivesAndUsingSpecifiedClassLoaderForResolution(
                SUPPORTED_PRIMITIVES, classLoader
        );

        return parser;
    }

    private String resolveExpression() {
        String expression = getExpression();
        return expression;
    }

    private String replaceBooleanOperators(String pcExpr) {
        String result = StringUtil.replace(pcExpr, " and ", " && ");
        result = StringUtil.replace(result, " or ", " || ");
        result = StringUtil.replace(result, " not ", " ! ");
        return result;
    }

    @Override
    public boolean isRuntime() {
        return false;
    }

    @Override
    public ClassFilter getClassFilter() {
        obtainPointcutExpression();
        return this;
    }

    @Override
    public MethodMatcher getMethodMatcher() {
        obtainPointcutExpression();
        return this;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }
}
